iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Mobile Development

從零開始學習 iOS系列 第 27

從零開始學習 Jetpack Compose Day26 - 專案實作 新增飲食記錄頁

  • 分享至 

  • xImage
  •  

在昨天的首頁中,我們完成了「今日營養統計」與「餐點清單」的顯示。

今天,我們要讓使用者能夠開啟「新增飲食記錄頁」,輸入餐點資料,並即時更新回首頁


今日目標

完成「新增記錄」頁面,讓使用者可以輸入:

  • 餐點名稱
  • 卡路里
  • 所屬餐別(早餐/午餐/晚餐/點心)

並透過 NavigationLink 進入新增畫面,填寫完畢後返回首頁更新清單。


調整資料來源

目前我們的資料是放在DashboardViewModel裡面。但如果「新增記錄頁」也要能操作資料,就會導致兩個 ViewModel 之間耦合。

因此,我們要建立一個MealRepository來集中管理資料來源。這樣未來若改成使用 SwiftData,也只需要修改 Repository 的實作即可。

import Foundation

class MealRepository: ObservableObject {
    static let shared = MealRepository()
    
    @Published private(set) var records: [MealRecord] = []
    
    private init() {}
    
    func addMeal(name: String, calories: Int, category: MealCategory) {
        let newMeal = MealRecord(
            name: name,
            calories: calories,
            category: category,
            date: Date()
        )
        records.append(newMeal)
    }
}

  • private(set):讓外部可以讀但不能直接改,確保資料一致性。
  • @Published:當資料更新時,畫面會自動重新整理。
  • static let :建立單例(Singleton),確保資料來源唯一。

調整DashboardViewModel

DashboardViewModel 只需要監聽 Repository 的變化,當 Repository 新增資料時,就會即時更新。

class DashboardViewModel: ObservableObject {
    @Published var records: [MealRecord] = []
    
    private let repository = MealRepository.shared
    
    init() {
        // 監聽 repository 的變化
        repository.$records
            .receive(on: RunLoop.main)
            .assign(to: &$records)
    }
    
    var totalCalories: Int {
        records.reduce(0) { $0 + $1.calories }
    }
    
    // 假資料
    var protein: Int { 60 }
    var carbs: Int { 150 }
    var fat: Int { 40 }
}

建立 ViewModel

接著我們來建立 AddMealViewModel,專門處理「新增餐點」邏輯,包含表單輸入與驗證。

class AddMealViewModel: ObservableObject {
    @Published var mealName: String = ""
    @Published var mealCalories: String = ""
    @Published var mealCategory: MealCategory = .breakfast
    @Published var showAlert: Bool = false
    @Published var addSuccess: Bool = false
    
    private let repository = MealRepository.shared
    
    
    func addMeal() {
        guard !mealName.trimmingCharacters(in: .whitespaces).isEmpty else {
            showAlert = true
            return
        }
        
        
        guard let calories = Int(mealCalories),
              !mealCalories.trimmingCharacters(in: .whitespaces).isEmpty else {
            showAlert = true
            return
        }
                
        repository.addMeal(
            name: mealName,
            calories: calories,
            category: mealCategory
        )
                
        // 清空輸入
        mealName = ""
        mealCalories = ""
        mealCategory = .breakfast
        addSuccess = true
    }
}
  • 輸入不完整時會彈出警示。
  • 儲存後會透過 addSuccess 通知畫面自動關閉。

建立新增記錄頁面

我們建立一個簡單的輸入表單 AddMealView

struct AddMealView: View {
    @Environment(\.dismiss) private var dismiss
    
    @StateObject private var viewModel = AddMealViewModel()

    var body: some View {
        Form {
            Section(header: Text("餐點資訊")) {
                TextField("餐點名稱", text: $viewModel.mealName)
                TextField("卡路里", text: $viewModel.mealCalories)
                    .keyboardType(.numberPad)
                
                Picker("餐別", selection: $viewModel.mealCategory) {
                    ForEach(MealCategory.allCases, id: \.self) { category in
                        Text(category.rawValue)
                    }
                }
            }
            
            Section {
                Button("儲存") {
                    viewModel.addMeal()
                }
                .frame(maxWidth: .infinity, alignment: .center)
            }
        }
        .alert("請輸入完整資訊", isPresented: $viewModel.showAlert) {
            Button("確定", role: .cancel) {}
        }
        .onChange(of: viewModel.addSuccess) { _, success in
            if success {
                viewModel.addSuccess = false
                dismiss()
            }
        }
        .navigationTitle("新增記錄")
    }
}

#Preview {
    AddMealView()
}

  • 透過 .onChange(of:) 監聽 viewModel.addSuccess,成功後自動關閉畫面。

從首頁導向新增頁面

回到 DashboardView,我們將「新增記錄」按鈕改成 NavigationLink,讓使用者能切換頁面。

Section {
    NavigationLink(destination: AddMealView(viewModel: viewModel)) {
        Label("新增記錄", systemImage: "plus.circle.fill")
            .font(.headline)
            .foregroundColor(.blue)
    }
    .frame(maxWidth: .infinity, alignment: .center)
}


預覽成果

https://github.com/jian-fu-hung/ithelp-2025/blob/main/image/Day26/%E8%9E%A2%E5%B9%95%E9%8C%84%E5%BD%B1%202025-10-11%20%E4%B8%8B%E5%8D%885.06.14.gif?raw=true

完成後,整個流程如下:

  1. 首頁顯示當日餐點與總卡路里
  2. 點擊「新增記錄」進入表單
  3. 輸入餐點資料並儲存
  4. 返回首頁,資料即時更新

今日小結

今天我們完成了飲食記錄 App 的第二個核心功能:

  • 使用 Form + Picker + TextField 建立輸入頁面
  • 利用 Repository 管理共用資料
  • 新增後即時更新首頁資料

明天,我們將繼續擴充 App 功能,加入「飲食紀錄清單頁」與「資料持久化(SwiftData)」,讓資料在關閉 App 後也能保留不遺失。


上一篇
從零開始學習 iOS Day25 - 專案實作 首頁
下一篇
從零開始學習 iOS Day27 - 專案實作 飲食紀錄清單頁
系列文
從零開始學習 iOS30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言